﻿using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;

namespace Sustainalytics.DynamicsCRMGateway
{
    public class CrmRepository<T> : ICrmRepository<T> where T : Entity
    {
        private readonly int _batchSize = 1000;

        private readonly CrmServiceContext _crmServiceContext;

        private static string EntityName { get { return typeof(T).GetEntityLogicalName(); } }

        public CrmRepository(CrmServiceContext crmServiceContext)
        {
            int batchSize;
            if (int.TryParse(ConfigurationManager.AppSettings["CrmBatchSize"], out batchSize))
            {
                _batchSize = batchSize;
            }

            _crmServiceContext = crmServiceContext;
        }

        public IEnumerable<T> ReadAll()
        {
            return _crmServiceContext.CreateQuery<T>().ToList();
        }

        public IEnumerable<T> ReadByIds(IEnumerable<Guid> ids)
        {
            var criteria = new FilterExpression();

            criteria.AddCondition(
                attributeName: EntityName + "id",
                conditionOperator: ConditionOperator.In,
                values: ids.Select(x => x.ToString()).ToArray());

            return ExecuteQuery(criteria).ToList();
        }

        public void Create(IEnumerable<T> entities)
        {
            var requests = entities.Select(entity => new CreateRequest { Target = entity });
            ExecuteMultipleRequests(requests);
        }

        public void Update(IEnumerable<T> entities)
        {
            var requests = entities.Select(entity => new UpdateRequest { Target = entity });
            ExecuteMultipleRequests(requests);
        }

        public void Delete(IEnumerable<Guid> ids)
        {
            var requests = ids.Select(id => new DeleteRequest { Target = new EntityReference(EntityName, id) });
            ExecuteMultipleRequests(requests);
        }

        private IEnumerable<T> ExecuteQuery(FilterExpression criteria = null)
        {
            var query = new QueryExpression()
            {
                EntityName = EntityName,
                Criteria = criteria,
                ColumnSet = new ColumnSet()
            };

            while (true)
            {
                var entityCollection = _crmServiceContext.RetrieveMultiple(query);
                foreach (var entity in entityCollection.Entities)
                {
                    yield return entity.ToEntity<T>();
                }

                // Check for more records, if it returns true.
                if (entityCollection.MoreRecords)
                {
                    // Increment the page number to retrieve the next page.
                    query.PageInfo.PageNumber++;
                    // Set the paging cookie to the paging cookie returned from current results.
                    query.PageInfo.PagingCookie = entityCollection.PagingCookie;
                }
                else
                {
                    // If no more records are in the result nodes, exit the loop.
                    break;
                }
            }
        }

        private void ExecuteMultipleRequests(IEnumerable<OrganizationRequest> requests)
        {
            foreach (var batch in requests.Batch(_batchSize))
            {
                var executeMultipleRequest = new ExecuteMultipleRequest()
                {
                    Settings = new ExecuteMultipleSettings() { ContinueOnError = false, ReturnResponses = false },
                    Requests = new OrganizationRequestCollection()
                };

                var items = batch.ToArray();
                executeMultipleRequest.Requests.AddRange(items);
                var response = (ExecuteMultipleResponse)_crmServiceContext.Execute(executeMultipleRequest);

                if (response.IsFaulted)
                {
                    ThrowCrmRequestException(items, response);
                }
            }
        }

        private static void ThrowCrmRequestException(OrganizationRequest[] items, ExecuteMultipleResponse response)
        {
            // We only have one responseItem because ExecuteMultipleSettings.ContinueOnError is false
            var responseItem = response.Responses.First();
            var request = items[responseItem.RequestIndex];
            var requestName = request.GetType().Name;
            var requestParameters = request.Parameters.ToDictionary(x => x.Key, x => x.Value);
            var faultMessage = responseItem.Fault.Message;
            var message = String.Format("The following error occurred executing request {0}: {1}", requestName, faultMessage);
            throw new CrmRequestException(message, requestParameters);
        }
    }
}
